﻿#region Copyright Notice
// 
// 
//  
//  Copyright (c) 2009-2011 Anindya Chatterjee
//  
//  Licensed under the Apache License, Version 2.0 (the "License");
//  you may not use this file except in compliance with the License.
//  You may obtain a copy of the License at
//  
//      http://www.apache.org/licenses/LICENSE-2.0
//  
//  Unless required by applicable law or agreed to in writing, software
//  distributed under the License is distributed on an "AS IS" BASIS,
//  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
//  See the License for the specific language governing permissions and
//  limitations under the License.
// 
// 
#endregion
using System;
using System.Collections;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Collections.Specialized;
using System.ComponentModel;
using System.Threading;
using AbstractClass.Collections.Generic;
using AbstractClass.Extensions;

namespace AbstractClass.Collections.ObjectModel
{
    /// <summary>
    /// Represents a wrapper for a data collection that provides notifications when items get added,
    /// removed or when the underlying collection is refreshed.
    /// </summary>
    /// <typeparam name="T">The type of the data in the underlying collection.</typeparam>
    /// <remarks>
    /// It works as a wrapper around the original collection keeping all the properties of the
    /// underlying collection intact unlike the <see cref="ObservableCollection{T}"/>.
    /// </remarks>
    public class NotifiableCollection<T> : INotifyCollectionChanged, INotifyPropertyChanged, ICollection<T>, ICollection,
                                           IDisposable
    {
        private readonly SimpleMonitor _monitor;
        private readonly ICollection<T> _underlyingCollection;
        private bool _disposed;
        private object _syncRoot;

        /// <summary>
        /// Initializes a new instance of the <see cref="NotifiableCollection&lt;T&gt;"/> class.
        /// </summary>
        /// <param name="collection">The collection to wrap with the new <see cref="NotifiableCollection&lt;T&gt;"/>.</param>
        public NotifiableCollection(ICollection<T> collection)
        {
            collection.ThrowIfNull("collection");
            _underlyingCollection = collection;
            _monitor = new SimpleMonitor();
        }

        /// <summary>
        /// Gets the underlying collection of this instance.
        /// </summary>
        /// <value>The underlying collection of this instance.</value>
        public ICollection<T> UnderlyingCollection
        {
            get { return _underlyingCollection; }
        }

        #region ICollection Members

        /// <summary>
        ///   Gets an object that can be used to synchronize access to the <see cref="NotifiableCollection&lt;T&gt;"/>.
        /// </summary>
        /// <value></value>
        /// <returns>
        ///   An object that can be used to synchronize access to the <see cref="NotifiableCollection&lt;T&gt;"/>.
        /// </returns>
        public object SyncRoot
        {
            get
            {
                _underlyingCollection.ThrowIfNull();
                if (_underlyingCollection is ICollection)
                    return ((ICollection) _underlyingCollection).SyncRoot;
                if (_syncRoot == null)
                {
                    Interlocked.CompareExchange<object>(ref _syncRoot, new object(), null);
                }
                return _syncRoot;
            }
        }

        /// <summary>
        ///   Gets a value indicating whether access to the <see cref="NotifiableCollection&lt;T&gt;"/> is synchronized (thread safe).
        /// </summary>
        /// <value></value>
        /// <returns><c>true</c> if access to the <see cref="NotifiableCollection&lt;T&gt;"/> is synchronized (thread safe); otherwise, <c>false</c>.
        /// </returns>
        public bool IsSynchronized
        {
            get
            {
                _underlyingCollection.ThrowIfNull();
                return _underlyingCollection is ICollection &&
                       ((ICollection) _underlyingCollection).IsSynchronized;
            }
        }

        void ICollection.CopyTo(Array array, int index)
        {
            CopyTo((T[]) array, index);
        }

        #endregion

        #region Implementation of IDisposable

        /// <summary>
        /// Releases all resources used by this <see cref="NotifiableCollection&lt;T&gt;"/>.
        /// </summary>
        public void Dispose()
        {
            Dispose(true);
            GC.SuppressFinalize(this);
        }

        /// <summary>
        /// Releases the unmanaged resources used by the <see cref="NotifiableCollection&lt;T&gt;"/>
        /// and optionally releases the managed resources.
        /// </summary>
        /// <param name="disposing"><c>true</c> to release both managed and unmanaged resources; <c>false</c>
        /// to release only unmanaged resources.</param>
        protected virtual void Dispose(bool disposing)
        {
            if (_disposed) return;
            if (disposing)
            {
                if (_monitor != null) _monitor.Dispose();
            }

            _disposed = true;
        }

        /// <summary>
        /// Releases unmanaged resources and performs other cleanup operations before the
        /// <see cref="NotifiableCollection&lt;T&gt;"/> is reclaimed by garbage collection.
        /// </summary>
        ~NotifiableCollection()
        {
            Dispose(false);
        }

        #endregion

        #region ICollection<T> Members

        /// <summary>
        ///   Gets the number of elements contained in the underlying collection.
        /// </summary>
        /// <returns>The number of elements contained in the underlying collection.</returns>
        public int Count
        {
            get
            {
                _underlyingCollection.ThrowIfNull();
                return _underlyingCollection.Count;
            }
        }

        /// <summary>
        ///   Gets a value indicating whether the underlying collection is read only.
        /// </summary>
        /// <value>
        ///   <c>true</c> if the underlying collection is read only; otherwise, <c>false</c>.
        /// </value>
        public bool IsReadOnly
        {
            get
            {
                _underlyingCollection.ThrowIfNull();
                return _underlyingCollection.IsReadOnly;
            }
        }

        /// <summary>
        /// Adds an item to the underlying collection.
        /// </summary>
        /// <param name = "item">The object to add to the underlying collection.
        /// </param>
        public void Add(T item)
        {
            _underlyingCollection.ThrowIfNull();
            CheckReentrancy();
            _underlyingCollection.Add(item);
            OnPropertyChanged("Count", "Item[]");
            OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, item, -1));
        }

        /// <summary>
        /// Clears the underlying collection by removing all of its elements.
        /// </summary>
        public void Clear()
        {
            _underlyingCollection.ThrowIfNull();
            CheckReentrancy();
            _underlyingCollection.Clear();
            OnPropertyChanged("Count", "Item[]");
            OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));
        }

        /// <summary>
        /// Determines whether the underlying collection contains the specified item.
        /// </summary>
        /// <param name="item">The item to search in the uderlying collection.</param>
        /// <returns>
        /// <c>true</c> if <paramref name="item" /> is found in the underlying collection; otherwise, <c>false</c>.
        /// </returns>
        public bool Contains(T item)
        {
            _underlyingCollection.ThrowIfNull();
            return _underlyingCollection.Contains(item);
        }

        /// <summary>
        /// Copies the elements of the underlying collection to an array of type <typeparamref name="T"/>
        /// , starting at the specified array index.
        /// </summary>
        /// <param name="array">The one-dimensional array of type <typeparamref name="T"/> that is the destination of
        /// the elements copied from the underlying collection. The array must have zero-based indexing.
        ///	 </param>
        /// <param name="arrayIndex">The zero-based index in array at which copying begins.</param>
        /// <remarks>The nodes are fetched in In-Order fashion.</remarks>
        public void CopyTo(T[] array, int arrayIndex)
        {
            _underlyingCollection.ThrowIfNull();
            _underlyingCollection.CopyTo(array, arrayIndex);
        }

        /// <summary>
        ///   Removes the first occurrence of a specific object from the underlying collection.
        /// </summary>
        /// <param name = "item">The object to remove from the underlying collection.
        /// </param>
        /// <returns>
        ///   <c>true</c> if <paramref name = "item" /> was successfully removed from the underlying collection;
        /// 		otherwise, <c>false</c>. This method also returns <c>false</c> if <paramref name = "item" /> is not found in the original
        /// 		underlying collection.
        /// </returns>
        public bool Remove(T item)
        {
            _underlyingCollection.ThrowIfNull();
            CheckReentrancy();
            bool result = _underlyingCollection.Remove(item);
            OnPropertyChanged("Count", "Item[]");
            OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Remove, item, -1));
            return result;
        }

        /// <summary>
        ///   Returns an enumerator that iterates through the underlying collection.
        /// </summary>
        /// <returns>
        ///   A <see cref = "IEnumerator&lt;T&gt;" /> that can be used to iterate through the underlying collection.
        /// </returns>
        public IEnumerator<T> GetEnumerator()
        {
            _underlyingCollection.ThrowIfNull();
            return _underlyingCollection.GetEnumerator();
        }

        IEnumerator IEnumerable.GetEnumerator()
        {
            return GetEnumerator();
        }

        #endregion

        #region INotifyCollectionChanged Members

        /// <summary>
        /// Occurs when an item is added, removed, changed, moved, or the
        /// entire underlying collection is refreshed.
        /// </summary>
        public event NotifyCollectionChangedEventHandler CollectionChanged;

        #endregion

        #region INotifyPropertyChanged Members

        /// <summary>
        /// Occurs when a property value changes.
        /// </summary>
        public event PropertyChangedEventHandler PropertyChanged;

        #endregion

        /// <summary>
        /// Raises the <see cref="CollectionChanged"/> event with the provided arguments.
        /// </summary>
        /// <param name="e">The <see cref="System.Collections.Specialized.NotifyCollectionChangedEventArgs"/> instance containing the event data.</param>
        protected virtual void OnCollectionChanged(NotifyCollectionChangedEventArgs e)
        {
            if (CollectionChanged == null) return;
            using (BlockReentrancy())
            {
                CollectionChanged(this, e);
            }
        }

        /// <summary>
        /// Raises the PropertyChanged event with the provided arguments.
        /// </summary>
        /// <param name="propertyNames">
        /// The names of the properties got changed.
        /// </param>
        protected virtual void OnPropertyChanged(params string[] propertyNames)
        {
            foreach (string t in propertyNames)
            {
                OnPropertyChanged(new PropertyChangedEventArgs(t));
            }
        }

        /// <summary>
        /// Raises the <see cref="PropertyChanged"/> event with the provided arguments.
        /// </summary>
        /// <param name="e">The <see cref="System.ComponentModel.PropertyChangedEventArgs"/> instance containing the event data.</param>
        protected virtual void OnPropertyChanged(PropertyChangedEventArgs e)
        {
            if (PropertyChanged != null)
            {
                PropertyChanged(this, e);
            }
        }

        /// <summary>
        /// Disallows reentrant attempts to change this collection.
        /// </summary>
        /// <returns>
        /// An <see cref="IDisposable"/> object that can be used to dispose of the object.
        /// </returns>
        protected IDisposable BlockReentrancy()
        {
            _monitor.Enter();
            return _monitor;
        }

        /// <summary>
        /// Checks for reentrant attempts to change this collection.
        /// </summary>
        /// <exception cref="InvalidOperationException">
        /// If there was a call to <see cref="BlockReentrancy"/> of which the <see cref="IDisposable"/>
        /// return value has not yet been disposed of. Typically, this means when there are additional
        /// attempts to change this collection during a <see cref="CollectionChanged"/> event. However, it depends on
        /// when derived classes choose to call <see cref="BlockReentrancy"/>.
        /// </exception>
        protected void CheckReentrancy()
        {
            if ((_monitor.Busy && (CollectionChanged != null)) && (CollectionChanged.GetInvocationList().Length > 0))
            {
                throw new InvalidOperationException(LanguageResource.NotifiableCollectionCannotChange);
            }
        }
    }
}